AWS IoT TwinMaker によるデジタルツインアプリケーションを AWS CDK で構築する 〜その4 コンポーネントタイプ設定編〜
こんにちは、CX 事業本部 Delivery 部の若槻です。
前回までは、AWS IoT TwinMaker でデジタルツインアプリケーションを構築する際に、デジタルツインとして表したい物理的な機器や概念のデジタル表現にコンテキストを提供するリソースである「エンティティ(Entity)」を AWS CDK で作成しました。
そして、そのエンティティのコンテキストに使用するデータを AWS の内外のサービスから取得可能とするリソースが、下記図の②に該当するコンポーネント(Component)です。コンポーネントタイプ(Component types)とも呼ばれます。 What is AWS IoT TwinMaker? - AWS IoT TwinMaker より引用
今回は、この AWS IoT TwinMaker のコンポーネントタイプの作成およびエンティティへの関連付けの設定を AWS CDK で行う方法を確認してみました。
やってみた
既定のコンポーネントタイプの確認
AWS サービスに接続する主要なユースケースのコンポーネントタイプは既定でワークスペースに作成されます。コンポーネントタイプを使用する場合は、既定のコンポーネントタイプから継承をするか、カスタムでコンポーネントタイプを作成することになります。
下記が既定で作成されているコンポーネントタイプの一覧です。
コンポーネントタイプ | AWS サービス | 説明 |
---|---|---|
com.amazon.athena.connector | Amazon Athena | Athena から表形式データを同期 |
com.amazon.iotsitewise.alarm | AWS IoT SiteWise | IoT SiteWise のアラームを同期 |
com.amazon.iotsitewise.connector | AWS IoT SiteWise | IoT SiteWise のアセットプロパティを同期 |
com.amazon.iotsitewise.connector.edgevideo | AWS IoT SiteWise | IoT SiteWise のエッジビデオアセットのプロパティを同期 |
com.amazon.iotsitewise.resourcesync | AWS IoT SiteWise | IoT SiteWise のリソースを同期 |
com.amazon.iottwinmaker.alarm.basic | AWS IoT TwinMaker | IoT TwinMaker 自身のアラームを抽出 |
com.amazon.iottwinmaker.documents | AWS IoT TwinMaker | ドキュメント URL の保管 |
com.amazon.iottwinmaker.parameters | AWS IoT TwinMaker | パラメーター値の保管 |
com.amazon.kvs.video | Amazon Kinesis Video Streams | Kinesis Video Streams からビデオを同期 |
コンポーネントタイプ一覧は AWS CLI でも取得できます。
$ aws iottwinmaker list-component-types --workspace-id CdkDemoWorkspace --region us-east-1 { "workspaceId": "CdkDemoWorkspace", "componentTypeSummaries": [ { "arn": "arn:aws:iottwinmaker:us-east-1:iottwinmakeraccount:workspace/AmazonOwnedTypesWorkspace/component-type/com.amazon.athena.connector", "componentTypeId": "com.amazon.athena.connector", "creationDateTime": 1668119172.16, "updateDateTime": 1668119172.16, "description": "Athena connector for syncing tabular data", "status": { "state": "ACTIVE", "error": {} } }, { "arn": "arn:aws:iottwinmaker:us-east-1:iotrociaccount:workspace/AmazonOwnedTypesWorkspace/component-type/com.amazon.iotsitewise.alarm", "componentTypeId": "com.amazon.iotsitewise.alarm", "creationDateTime": 1657733017.925, "updateDateTime": 1657733017.925, "description": "iotsitewise alarm native component type", "status": { "state": "ACTIVE", "error": {} } }, { "arn": "arn:aws:iottwinmaker:us-east-1:iotrociaccount:workspace/AmazonOwnedTypesWorkspace/component-type/com.amazon.iotsitewise.connector", "componentTypeId": "com.amazon.iotsitewise.connector", "creationDateTime": 1636763132.467, "updateDateTime": 1636763132.467, "description": "IoT SiteWise connector for syncing asset properties", "status": { "state": "ACTIVE", "error": {} } }, { "arn": "arn:aws:iottwinmaker:us-east-1:iotrociaccount:workspace/AmazonOwnedTypesWorkspace/component-type/com.amazon.iotsitewise.connector.edgevideo", "componentTypeId": "com.amazon.iotsitewise.connector.edgevideo", "creationDateTime": 1636763134.069, "updateDateTime": 1636763134.069, "description": "IoT SiteWise connector for syncing edgevideo asset properties", "status": { "state": "ACTIVE", "error": {} } }, { "arn": "arn:aws:iottwinmaker:us-east-1:iottwinmakeraccount:workspace/AmazonOwnedTypesWorkspace/component-type/com.amazon.iotsitewise.resourcesync", "componentTypeId": "com.amazon.iotsitewise.resourcesync", "creationDateTime": 1666719668.0, "updateDateTime": 1666719668.0, "description": "Base component type for syncing resources from IoT SiteWise", "status": { "state": "ACTIVE", "error": {} } }, { "arn": "arn:aws:iottwinmaker:us-east-1:iotrociaccount:workspace/AmazonOwnedTypesWorkspace/component-type/com.amazon.iottwinmaker.alarm.basic", "componentTypeId": "com.amazon.iottwinmaker.alarm.basic", "creationDateTime": 1636763135.967, "updateDateTime": 1636763135.967, "description": "Abstract alarm component type", "status": { "state": "ACTIVE", "error": {} } }, { "arn": "arn:aws:iottwinmaker:us-east-1:iotrociaccount:workspace/AmazonOwnedTypesWorkspace/component-type/com.amazon.iottwinmaker.documents", "componentTypeId": "com.amazon.iottwinmaker.documents", "creationDateTime": 1636763130.797, "updateDateTime": 1636763130.797, "description": "Component type for storing document urls", "status": { "state": "ACTIVE", "error": {} } }, { "arn": "arn:aws:iottwinmaker:us-east-1:iotrociaccount:workspace/AmazonOwnedTypesWorkspace/component-type/com.amazon.iottwinmaker.parameters", "componentTypeId": "com.amazon.iottwinmaker.parameters", "creationDateTime": 1636763138.067, "updateDateTime": 1636763138.067, "description": "Component type for storing parameter values", "status": { "state": "ACTIVE", "error": {} } }, { "arn": "arn:aws:iottwinmaker:us-east-1:iottwinmakeraccount:workspace/AmazonOwnedTypesWorkspace/component-type/com.amazon.kvs.video", "componentTypeId": "com.amazon.kvs.video", "creationDateTime": 1661368377.345, "updateDateTime": 1661368377.345, "description": "Component type for syncing video from kinesis video stream", "status": { "state": "ACTIVE", "error": {} } } ] }
今回は Amazon Athena でデータを取得できる com.amazon.athena.connector
を継承したコンポーネントタイプを CDK で作成してみます。
最終的な CDK コード
最終的には以下の CDK コードでエンティティを構築することができました。
import { Construct } from 'constructs'; import { aws_s3, aws_iam, aws_iottwinmaker, aws_athena, Stack, RemovalPolicy, } from 'aws-cdk-lib'; import * as glue_alpha from '@aws-cdk/aws-glue-alpha'; interface IoTTwinMakerConstructProps { readonly dataSourceBucket: aws_s3.Bucket; readonly queryResultBucket: aws_s3.Bucket; readonly athenaWorkGroup: aws_athena.CfnWorkGroup; readonly glueDatabase: glue_alpha.Database; readonly glueTable: glue_alpha.S3Table; } export class IoTTwinMakerConstruct extends Construct { constructor(scope: Construct, id: string, props: IoTTwinMakerConstructProps) { super(scope, id); const { dataSourceBucket, queryResultBucket, athenaWorkGroup, glueDatabase, glueTable, } = props; const accountId = Stack.of(this).account; const region = Stack.of(this).region; const workspaceId = 'CdkDemoWorkspace'; // ワークスペース用リソース保管バケット const workspaceResourceBucket = new aws_s3.Bucket( this, 'WorkspaceResourceBucket', { removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, } ); // IoT TwinMaker ワークスペース実行ロール用プリンシパル const principal = new aws_iam.ServicePrincipal( 'iottwinmaker.amazonaws.com' ).withConditions({ StringEquals: { 'aws:SourceAccount': accountId, }, StringLike: { 'aws:SourceArn': `arn:aws:iottwinmaker:${region}:${accountId}:workspace/${workspaceId}`, }, }); // ワークスペース実行ロール const workspaceExecutionRole = new aws_iam.Role( this, 'WorkspaceExecutionRole', { assumedBy: principal, inlinePolicies: { readWorkspaceResourceBucket: aws_iam.PolicyDocument.fromJson({ Version: '2012-10-17', Statement: [ // 少なくともワークスペース作成時に必要な権限 { Effect: 'Allow', Action: [ 's3:GetBucket', 's3:GetObject', 's3:ListBucket', 's3:PutObject', 's3:ListObjects', 's3:ListObjectsV2', 's3:GetBucketLocation', ], Resource: [ workspaceResourceBucket.bucketArn, workspaceResourceBucket.arnForObjects('*'), ], }, // ワークスペース削除時に必要な権限 { Effect: 'Allow', Action: ['s3:DeleteObject'], Resource: [ workspaceResourceBucket.arnForObjects( `DO_NOT_DELETE_WORKSPACE_${workspaceId}` ), ], }, // タイプコンポーネントが Amazon Athena でデータを取得する際に必要な権限 { Effect: 'Allow', Action: [ 'athena:GetQueryExecution', 'athena:GetQueryResults', 'athena:GetTableMetadata', 'athena:GetWorkGroup', 'athena:StartQueryExecution', 'athena:StopQueryExecution', ], Resource: [ `arn:aws:athena:${region}:${accountId}:workgroup/${athenaWorkGroup.name}`, `arn:aws:athena:${region}:${accountId}:datacatalog/AwsDataCatalog`, ], }, { Effect: 'Allow', Action: [ 'glue:GetTable', 'glue:GetTables', 'glue:GetDatabase', 'glue:GetDatabases', ], Resource: [ glueDatabase.catalogArn, glueDatabase.databaseArn, glueTable.tableArn, ], }, ], }), }, } ); // タイプコンポーネントが Amazon Athena でデータを取得する際に必要な権限 dataSourceBucket.grantRead(workspaceExecutionRole); queryResultBucket.grantReadWrite(workspaceExecutionRole); // ワークスペース const cdkDemoWorkspace = new aws_iottwinmaker.CfnWorkspace( this, 'CdkDemoWorkspace', { workspaceId, s3Location: workspaceResourceBucket.bucketArn, role: workspaceExecutionRole.roleArn, } ); const cdkDemoWorkspaceId = cdkDemoWorkspace.workspaceId; // コンポーネントタイプ const cdkDemoAthenaConnector = new aws_iottwinmaker.CfnComponentType( this, 'CdkDemoAthenaConnector', { workspaceId: cdkDemoWorkspaceId, isSingleton: false, componentTypeId: 'cdk.demo.athena.connector', propertyDefinitions: { athenaDataSource: { defaultValue: { stringValue: 'AwsDataCatalog', }, }, athenaDatabase: { defaultValue: { stringValue: glueDatabase.databaseName, }, }, athenaExternalIdColumnName: { defaultValue: { stringValue: 'device_id', }, }, athenaTable: { defaultValue: { stringValue: glueTable.tableName, }, }, athenaWorkgroup: { defaultValue: { stringValue: athenaWorkGroup.name, }, }, device_id: { dataType: { type: 'STRING', }, isTimeSeries: false, isRequiredInEntity: false, isExternalId: false, isStoredExternally: true, }, timestamp: { dataType: { type: 'STRING', }, isTimeSeries: false, isRequiredInEntity: false, isExternalId: false, isStoredExternally: true, }, value: { dataType: { type: 'INTEGER', }, isTimeSeries: false, isRequiredInEntity: false, isExternalId: false, isStoredExternally: true, }, }, extendsFrom: ['com.amazon.athena.connector'], // 継承元のコンポーネントタイプの指定 propertyGroups: { tabularPropertyGroup: { groupType: 'TABULAR', propertyNames: ['device_id', 'value', 'timestamp'], }, }, } ); // ワークスペースとコンポーネントタイプの依存関係を設定 cdkDemoAthenaConnector.addDependency(cdkDemoWorkspace); // エンティティの作成 const sampleParentEntity = new aws_iottwinmaker.CfnEntity( this, 'SampleParentEntity', { entityName: 'sampleParentEntity1', entityId: 'sampleParentEntity1', workspaceId: cdkDemoWorkspaceId, components: { AthenaConnector: { componentTypeId: cdkDemoAthenaConnector.componentTypeId, properties: { athenaComponentExternalId: { value: { stringValue: 'd001', }, }, }, }, }, } ); // エンティティとコンポーネントタイプの依存関係を設定 sampleParentEntity.addDependency(cdkDemoAthenaConnector); // 子エンティティおよびシーンの作成は省略 } }
import { aws_s3, aws_athena, RemovalPolicy, CfnOutput } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as glue_alpha from '@aws-cdk/aws-glue-alpha'; export class AthenaConstruct extends Construct { public readonly dataSourceBucket: aws_s3.Bucket; public readonly queryResultBucket: aws_s3.Bucket; public readonly athenaWorkGroup: aws_athena.CfnWorkGroup; public readonly glueTable: glue_alpha.S3Table; public readonly glueDatabase: glue_alpha.Database; constructor(scope: Construct, id: string) { super(scope, id); // データソース用 S3 バケット const dataSourceBucket = new aws_s3.Bucket(this, 'DataSourceBucket', { removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, }); this.dataSourceBucket = dataSourceBucket; // データソース用 S3 バケット名を出力 new CfnOutput(this, 'DataSourceBucketName', { value: dataSourceBucket.bucketName, }); // Athena クエリ結果保管用 S3 バケット const queryResultBucket = new aws_s3.Bucket(this, 'QueryResultBucket', { removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, }); this.queryResultBucket = queryResultBucket; // Athena ワークグループ const workGroup = new aws_athena.CfnWorkGroup(this, 'WorkGroup', { name: 'cdkTwinmakerWorkGroup', workGroupConfiguration: { resultConfiguration: { outputLocation: queryResultBucket.s3UrlForObject('/result-data'), }, }, recursiveDeleteOption: true, }); this.athenaWorkGroup = workGroup; // データソース用 Glue データベース, テーブル const glueDatabase = new glue_alpha.Database(this, 'GlueDatabase'); const glueTable = new glue_alpha.S3Table(this, 'GlueTable', { database: glueDatabase, columns: [ { name: 'device_id', type: glue_alpha.Schema.STRING, }, { name: 'value', type: glue_alpha.Schema.INTEGER, }, { name: 'timestamp', type: glue_alpha.Schema.TIMESTAMP, }, ], dataFormat: glue_alpha.DataFormat.CSV, bucket: dataSourceBucket, s3Prefix: 'data/', }); this.glueDatabase = glueDatabase; this.glueTable = glueTable; } }
import { Stack } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { AthenaConstruct } from './construct/athena'; import { IoTTwinMakerConstruct } from './construct/iottwinmaker'; export class CdkSampleStack extends Stack { constructor(scope: Construct, id: string) { super(scope, id); // Athena リソースの作成 const athena = new AthenaConstruct(this, 'Athena'); // IoT TwinMaker リソースの作成 new IoTTwinMakerConstruct(this, 'IoTTwinMaker', athena); } }
- コードの前半で、Amazon Athena で S3 バケット上のデータをクエリするために必要なリソースおよび実行権限を作成しています。
- コンポーネントタイプの作成では L1 Construct である
CfnComponentType
を使用します。propertyDefinitions
で、コンポーネントタイプで使用するリソースおよびデータスキーマの定義をします。athenaExternalIdColumnName
で、device_id
を ExternalId として指定しています。extendsFrom
で、継承するコンポーネントタイプを指定します。propertyGroups
で、データのスキーマとなる文字列を指定します。
- コンポーネントタイプのエンティティへの関連付けの設定は
CfnEntity
のcomponents
で行います。AthenaConnector
は任意の名前をキーとして指定します。- ExternalId の値として
d001
を指定しています。これによりこのエンティティではdevice_id
がd001
のデータのみが取得されます。
CDK デプロイすると、コンポーネントタイプが作成されていることがコンソールから確認できます。
また、エンティティにコンポーネントタイプが設定されていることも確認できます。
動作確認
データの準備
データソース用 S3 バケットにデータをアップロードします。
d001,30,1695740400000 d001,25,1695826800000 d002,10,1695740400000 d003,-5,1695654000000
aws s3 cp data.csv s3://${dataSourceBucketName}/data/data.csv
コンソールから Athena でクエリできることを確認します。
データの取得
コンソールからデータをテスト取得してみます。
取得したいスキーマを選択して、Run test をクリック。
すると先ほどバケットに保存したデータが Athena で取得できました。
[ [ { "device_id": { "stringValue": "d001" }, "timestamp": { "stringValue": "2023-09-26 15:00:00.000" }, "value": { "integerValue": 30 } }, { "device_id": { "stringValue": "d001" }, "timestamp": { "stringValue": "2023-09-27 15:00:00.000" }, "value": { "integerValue": 25 } } ] ]
CfnComponentTypeProps 型定義
参考までに、CfnComponentType
のプロパティの型定義一覧は次のようになります。
/** * Properties for defining a `CfnComponentType` * * @struct * @stability external * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-componenttype.html */ export interface CfnComponentTypeProps { /** * The ID of the component type. * * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-componenttype.html#cfn-iottwinmaker-componenttype-componenttypeid */ readonly componentTypeId: string; /** * The description of the component type. * * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-componenttype.html#cfn-iottwinmaker-componenttype-description */ readonly description?: string; /** * The name of the parent component type that this component type extends. * * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-componenttype.html#cfn-iottwinmaker-componenttype-extendsfrom */ readonly extendsFrom?: Array<string>; /** * An object that maps strings to the functions in the component type. * * Each string in the mapping must be unique to this object. * * For information on the FunctionResponse object see the [FunctionResponse](https://docs.aws.amazon.com//iot-twinmaker/latest/apireference/API_FunctionResponse.html) API reference. * * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-componenttype.html#cfn-iottwinmaker-componenttype-functions */ readonly functions?: cdk.IResolvable | Record<string, CfnComponentType.FunctionProperty | cdk.IResolvable>; /** * A boolean value that specifies whether an entity can have more than one component of this type. * * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-componenttype.html#cfn-iottwinmaker-componenttype-issingleton */ readonly isSingleton?: boolean | cdk.IResolvable; /** * An object that maps strings to the property definitions in the component type. * * Each string in the mapping must be unique to this object. * * For information about the PropertyDefinitionResponse object, see the [PropertyDefinitionResponse](https://docs.aws.amazon.com//iot-twinmaker/latest/apireference/API_PropertyDefinitionResponse.html) API reference. * * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-componenttype.html#cfn-iottwinmaker-componenttype-propertydefinitions */ readonly propertyDefinitions?: cdk.IResolvable | Record<string, cdk.IResolvable | CfnComponentType.PropertyDefinitionProperty>; /** * An object that maps strings to the property groups in the component type. * * Each string in the mapping must be unique to this object. * * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-componenttype.html#cfn-iottwinmaker-componenttype-propertygroups */ readonly propertyGroups?: cdk.IResolvable | Record<string, cdk.IResolvable | CfnComponentType.PropertyGroupProperty>; /** * The ComponentType tags. * * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-componenttype.html#cfn-iottwinmaker-componenttype-tags */ readonly tags?: Record<string, string>; /** * The ID of the workspace. * * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-componenttype.html#cfn-iottwinmaker-componenttype-workspaceid */ readonly workspaceId: string; }
トラブルシューティング
Error occurred during operation 'ErrorDetails(Code=VALIDATION_ERROR, Message=One or more components are in Error state.)'.
当初、コンポーネントタイプの作成で athenaDataSource
で Glue カタログ名を設定すべきところを次のように glueDatabase.catalogId
指定していました。これだとカタログの AWS アカウント ID が設定されてしまいます。
// コンポーネントタイプ const cdkDemoAthenaConnector = new aws_iottwinmaker.CfnComponentType( this, 'CdkDemoAthenaConnector', { workspaceId: cdkDemoWorkspace.workspaceId, isSingleton: false, componentTypeId: 'cdk.demo.athena.connector', propertyDefinitions: { athenaDataSource: { defaultValue: { stringValue: glueDatabase.catalogId, // 誤った指定 }, }, // 省略 }, // 省略 } );
上記の誤った指定をした場合でも CDK デプロイによるコンポーネントタイプの作成はエラー無く完了します。
しかしコンポーネントのスキーマは正常に設定されなくなります。次のように空となってしまいます。
またエンティティへのコンポーネントタイプの関連付けが Error occurred during operation 'ErrorDetails(Code=VALIDATION_ERROR, Message=One or more components are in Error state.)'.
というエラーとなります。
このエラーは CDK デプロイ時に発生しますが、ロールバックは上手く行われずエンティティへ不正な設定が反映される場合もあります。その場合は次のようにコンソール上でも同じエラーが表示されます。
また IoT TwinMaker ワークスペースの実行ロールに、Athena や Glue などのリソースへのアクセス権限が付与されていない場合も同様のエラーが発生します。
本エラーが発生した場合はコンポーネントタイプのプロパティ定義を見直してみましょう。
参考
以上